/*
 * Copyright (C) 2024 techarts.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.techarts.copycat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import cn.techarts.copycat.core.Frame;
import cn.techarts.copycat.core.Session;

/**
 * The server bootstrap class
 * 
 * @author rocwon@gmail.com
 * 
 */
public class Booster<T extends Frame> implements AutoCloseable{
	private Monitor monitor = null;
	private Context<T> context = null;
	private ExecutorService executorService;
    private ExecutorService workerExecutorService;
    private AsynchronousChannelGroup channelGroup;
    private AsynchronousServerSocketChannel serverSocketChannel;
 
    public Booster(Context<T> context) throws Panic{
    	try {
    		this.context = context.requiredProperties();
            this.monitor = new Monitor(context.samplePeriod());
            if(context.virtualThreadEnabled()) {
            	executorService = Executors.newVirtualThreadPerTaskExecutor();
            	workerExecutorService = Executors.newVirtualThreadPerTaskExecutor();
            }else {
            	executorService = Executors.newCachedThreadPool();
            	workerExecutorService = Executors.newFixedThreadPool(context.maxThreads());
            }
            channelGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
            serverSocketChannel = openServerSocket(context.tlsEnabled(), channelGroup);
            this.setServerSocketOptions();
            serverSocketChannel.bind(new InetSocketAddress(context.port()));
            serverSocketChannel.accept(serverSocketChannel, new ConnectionAcceptor());
        } catch (IOException e) {
            throw new Panic(e, "Failed to start the server.");
        }
    }
    
    class ConnectionAcceptor implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>{
		@Override
		public void completed(AsynchronousSocketChannel client, AsynchronousServerSocketChannel server) {
			var session = new Session<T>(client, context, monitor);
			if(context.virtualThreadEnabled()) {
				workerExecutorService.submit(session);
			}else {
				workerExecutorService.execute(session);
			}
			server.accept(serverSocketChannel, this);
		}

		@Override
		public void failed(Throwable e, AsynchronousServerSocketChannel server) {
			throw new Panic(e, "The server failed to accept connection.");
		}
    }
    
    /**
     * @see close
     */
    private void releaseResourcesAndCleanup() throws Panic {
    	try {
	    	if(channelGroup == null) return;
	    	if(channelGroup.isShutdown()) return;
	    	this.channelGroup.shutdown();
	    	this.executorService.shutdown();
	    	this.workerExecutorService.shutdown();
	  }catch(Exception e) {
    		throw new Panic(e, "Failed to release resource while shutdown.");
    	}
    }
    
    private void setServerSocketOptions() throws IOException {
    	if(context.rcvBuffer() > 0) {
    		serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, context.rcvBuffer());
    	}
    	this.context.rcvBuffer(serverSocketChannel.getOption(StandardSocketOptions.SO_RCVBUF));
    	
    	if(context.addrReuseEnabled()) { //default: disabled
    		serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
    	}
    	if(context.keepAliveEnabled()) { //default: disabled
    		serverSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
    	}
    }
    
    private AsynchronousServerSocketChannel openServerSocket(boolean tlsEnabled, AsynchronousChannelGroup group) throws IOException {
    	if(!tlsEnabled) {
    		return AsynchronousServerSocketChannel.open(group);
    	}else { //TODO Needs an implementation of SSL/TLS
    		return AsynchronousServerSocketChannel.open(group);
    	}
    }
    
    public Monitor getMonitorObject() {
    	return this.monitor;
    }
    
    @Override
    public void close() {
    	releaseResourcesAndCleanup();
    }
}